let http = require('http')
let fs = require('fs')
let url = require('url')
let path = require('path')
let promisify = require('util').promisify
let mime = require('mime-types')

let defaults = {
  mime: {
    html: 'text/html; charset=utf-8'
  },
  type: {
    FILE: 'file',
    NOT_FOUND: '404',
    DIRECTORY: 'directory'
  }
}

let { mime: defaultMimeMap, type: defaultTypeMap } = defaults

let cwd = process.cwd()

let lstat = promisify(fs.lstat)
let exists = promisify(fs.exists)
let realpath = promisify(fs.realpath)
let readFile = promisify(fs.readFile)
let readdir = promisify(fs.readdir)

main()

function main() {
  let port = 5500

  let options = {
    vd: '/static'
  }

  http.createServer(static(options)).listen({ port })
}

function static(options) {
  let { vd } = options

  return async (req, res) => {
    // 1  url-resolver
    let { pathname, isVdActived } = resolver.resolve(req.url, vd)

    // 2 fs-reader
    let result = await reader.read(pathname)

    // 3 transform
    let renderableData = await transformer.transform(result, {
      pathname,
      isVdActived,
      vd
    })

    // 4 renderer -> data|html
    let data = renderer.render(renderableData)

    if (renderableData.data.mime) {
      res.setHeader('Content-Type', renderableData.data.mime)
    }

    res.end(data)
  }
}

let resolver = {
  resolve: (reqUrl, vd) => {
    let { pathname } = url.parse(reqUrl)

    pathname = decodeURIComponent(pathname)

    let isVdActived = reqUrl.startsWith(vd)

    if (isVdActived) {
      pathname = pathname.replace(vd, '')
    }

    return { pathname, isVdActived }
  }
}

let reader = {
  /**
   * @return {object}
   * { type: '404|file|directory', data: { mime, content, files } }
   */
  read: async pathname => {
    let filename = path.join(cwd, pathname)

    let { isFile, isDirectory } = await readFileInfo(filename)

    let type = isDirectory
      ? defaultTypeMap.DIRECTORY
      : isFile
      ? defaultTypeMap.FILE
      : defaultTypeMap.NOT_FOUND

    let data = {}

    if (type === defaultTypeMap.FILE) {
      data.mime = mime.contentType(path.basename(filename))

      data.content = await readFile(filename)
    } else if (type === defaultTypeMap.DIRECTORY) {
      data.mime = defaults.mime.html

      data.files = await readdir(filename)
    }

    return { type, data }
  }
}

let transformer = {
  transform: async (result, { pathname, isVdActived, vd }) => {
    let { type, data = {} } = result

    if (type === defaultMimeMap.NOT_FOUND) {
      data.mime = defaults.mime.html

      data.content = 'Page 404'
    } else if (type === defaultTypeMap.DIRECTORY) {
      let { fileMapList, breadCrumbList } = await prepareDirectoryData(
        data.files,
        { pathname, isVdActived, vd }
      )

      data.fileMapList = fileMapList
      data.breadCrumbList = breadCrumbList
    }

    return result
  }
}

let renderer = {
  render: ({ type, data = {} }) => {
    let { fileMapList, breadCrumbList } = data

    if (type === defaultTypeMap.DIRECTORY) {
      let html = renderDirectory({ fileMapList, breadCrumbList })

      return html
    }

    return data.content
  },

  renderToHtml: ({ title = '', nav = '', content = '' } = {}) => {
    return `
      <html>
        <head>
          <meta charset="utf-8" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
          />
          <title>directory ${title}</title>
          <link rel="stylesheet" href="/src/assets/style.css" />
          <link rel="stylesheet" href="/src/assets/icon.css" />
        </head>
        <body class="directory">
          ${nav}
          ${content}
        </body>
      </html>
    `
  }
}

async function readFileInfo(filename) {
  let isExist = await exists(filename)

  if (!isExist) return { isExist }

  let fileInfo = await getFileInfo(filename)

  return { ...fileInfo }
}

async function getFileInfo(filename) {
  let stat = await lstat(filename)

  let isFile = stat.isFile()
  let isDirectory = stat.isDirectory()
  let isSymbolicLink = stat.isSymbolicLink()

  if (isSymbolicLink) {
    let realFlename = await realpath(filename)

    return getFileInfo(realFlename)
  }

  return { isFile, isDirectory }
}

async function prepareDirectoryData(files, { pathname, isVdActived, vd }) {
  if (pathname !== '' && pathname !== '/') {
    files.unshift('..')
  }

  let fileMapList = files
    .map(file => ({ name: file }))
    .map(item => {
      let relativePath = path.join(pathname, item.name)
      let filename = path.join(cwd, relativePath)
      return {
        ...item,
        relativePath,
        filename
      }
    })
    .map(item => {
      let href = isVdActived
        ? path.join(vd, item.relativePath)
        : item.relativePath
      return {
        ...item,
        href
      }
    })

  fileMapList = await Promise.all(fileMapList.map(addClzNameProp))

  let breadCrumbList = createBreadCrumbList(pathname, { isVdActived, vd })

  return {
    fileMapList,
    breadCrumbList
  }
}

async function addClzNameProp(obj) {
  let { filename } = obj

  let { isFile, isDirectory } = await getFileInfo(filename)

  let clzName = 'icon'

  if (isDirectory) {
    clzName += ' icon-directory'
  } else if (isFile) {
    let contentType = mime.lookup(filename)
    if (contentType) {
      clzName += ' icon-' + contentType.replace(/[/|.]/g, '-')
    } else {
      clzName += ' icon-default'
    }
  }

  return {
    ...obj,
    clzName
  }
}

/**
 * @examples
 * /src/views/home
 * ['/src', '/src/views/', '/src/views/home']
 * isVdActived, prefixWithVd
 */
function createBreadCrumbList(pathname, { isVdActived, vd }) {
  let breadCrumbList = pathname
    .split('/')
    .filter(Boolean)
    .map(_ => '/' + _)
    .map((value, index, array) => {
      let href = value
      for (let i = index - 1; i >= 0; i--) {
        href = array[i] + href
      }
      return { href, name: value.slice(1), directory: value }
    })
    .map(item => {
      let { href } = item
      return {
        ...item,
        href: isVdActived ? path.join(vd, href) : href
      }
    })

  // 增加首页索引以及链接
  breadCrumbList.unshift({
    href: isVdActived ? path.join(vd, '/') : '/',
    directory: '/',
    name: '~'
  })

  // 最后一级面包屑不可点击
  breadCrumbList = breadCrumbList.map((item, index, array) => {
    if (index === array.length - 1) {
      return {
        ...item,
        href: 'javascript:;'
      }
    }
    return item
  })

  return breadCrumbList
}

function renderDirectory({ fileMapList = [], breadCrumbList = [] } = {}) {
  if (!fileMapList.length && !breadCrumbList.length) {
    return renderer.renderToHtml()
  }

  let title = ''
  let nav = ''

  if (breadCrumbList.length) {
    title = breadCrumbList[breadCrumbList.length - 1].name

    nav = `
      <h1>
        ${breadCrumbList
          .map(item => {
            let { href, name } = item
            return `
            <a href="${href}">${name}</a> /
          `
          })
          .join('')}
      </h1>
    `
  }

  let content = ''

  if (fileMapList.length) {
    content = `
      <ul id="files">
        ${fileMapList
          .map(({ name, href, clzName }) => {
            return `
            <li>
              <a class="${clzName}" href="${href}">
                <span class="name">${name}</span>
              </a>
            </li>
          `
          })
          .join('')}
      </ul>
    `
  }

  return renderer.renderToHtml({ title, nav, content })
}
